import {gmStore, reqGM} from './gm.js';

// Jquery的扩展，本页的其他函数依赖下面拓展的方法
export function jqExtend($){
	if (!$){
		return;
	}
	const fn = $.fn;
	
	fn.extend({
		// 将html解析成document对象，然后包裹成jQuery对象
		_parseDoc(html){
			return win.$(Document.parseHTMLUnsafe(html));
		},
	});
	
	// ================== 数组函数式编程操作 =================
	// 回调函数的签名为 f(element,index)
	fn.extend({
		_each(f){
			return this.each(reverseArgs(f));
		},
		_map(f){
			return this.map(reverseArgs(f));
		},
		_filter(f){
			if (typeof f!=='function'){ // f有可能是选择器
				return this.filter(f);
			}
			return this.filter(reverseArgs(f));
		},
	});
	
	// 交换函数的第一个和第二个参数
	function reverseArgs(f){
		return function(a, b){
			return f.apply(this, [b, a]);
		};
	}
	
	// ============== DOM增删改查 ====================
	fn.extend({
		// jQuery没有类似DOM.contains的方法
		_contains(selector){
			return this.find(selector).length>0;
		},
		// 后续兄弟元素中匹配的第一个
		_nextMatch(selector){
			return this.nextAll(selector).eq(0);
		},
		// 包含根元素的查找结果
		_findWithRoot(selector){
			return this.filter(selector).add(this.find(selector));
		},
		// 切换DOM属性的布尔值状态
		_toggleProp(prop){
			return this._each(e=>e[prop] = !e[prop]);
		},
		// 使用css文本或对象更新元素的样式
		_setCss(css){
			return this._each(e=>{
				setCss(e, css);
			});
		},
		// 返回所有元素外层html连接而成的字符串，或者原地设置每个元素的外层html
		_outerHtml(html){
			// 返回所有元素外层html连接而成的字符串
			if (!html){
				return this.get().values(e=>e.outerHTML).join('');
			}
			// 设置每个元素的外层html
			for (let i = 0; i<this.length; i++){
				this[i] = $(html)[0];
			}
			return this;
		},
		// 原地修改元素的标签名称，事件会丢失
		_setTag(tag){
			for (let i = 0; i<this.length; i++){
				const e = this[i];
				const newEl = $(`<${tag}>`)[0];
				newEl.innerHTML = e.innerHTML; // 复制innerHTML
				for (const attribute of e.attributes){ // 拷贝所有属性值
					newEl.setAttribute(attribute.name, attribute.value);
				}
				
				this[i].replaceWith(newEl); // 替换页面上的DOM元素
				this[i] = newEl; // 替换jQuery对象中的DOM元素
			}
			return this;
		},
	});
	
	// ============== 事件触发 ====================
	// 注意：jquery的事件系统和原生的事件系统兼容性很差
	fn.extend({
		// 使用原生方式触发事件，可以使用props增加自定义属性（不能指定Event的只读属性）
		_trigger(eventName, props = {}){
			return this._each(e=>{
				const event = new Event(eventName, {
					bubbles:true,
					cancelable:true,
					composed:true,
				});
				Object.assign(event, props);
				e.dispatchEvent(event);
			});
		},
		// 模拟点击事件
		_click(){
			return this._each(e=>{
				['pointerdown', 'pointerup', 'click'].forEach(type=>{
					simulatePointerEvent(e, 'pointerdown');
					simulatePointerEvent(e, 'pointerup');
					simulatePointerEvent(e, 'click');
				});
			});
		},
		// 模拟输入事件，可能存在框架劫持value属性的情况，遇到再解决
		_input(text){
			let view = window;
			if (typeof unsafeWindow!=='undefined'){
				view = unsafeWindow;
			}
			return this._each(e=>{
				$(e).val(text);
				// 触发input事件
				e.dispatchEvent(new InputEvent('input', {
					bubbles:true,
					cancelable:true,
					composed:true,
					view:view,
					inputType:'insertText',
					data:text,
				}));
				// 触发change事件
				e.dispatchEvent(new Event('change', {
					bubbles:true,
					cancelable:true,
					composed:true,
				}));
			});
		},
		
		// 拖拽元素，y的方向向上
		_drag(deltaX, deltaY){
			return this._each(e=>{
				dragElement(e, deltaX, deltaY);
			});
		},
	});
}

// 模拟拖拽元素
function dragElement(selector, deltaX, deltaY){
	const el = $(selector)[0];
	if (!el){
		return;
	}
	deltaY = -deltaY; // 让y的方向向上
	const rect = el.getBoundingClientRect();
	const startX = rect.left+rect.width/2; // 初始点击元素中心
	const startY = rect.top+rect.height/2;
	let view = window;
	if (typeof unsafeWindow!=='undefined'){
		view = unsafeWindow;
	}
	
	const eventOptions = {
		bubbles:true,
		cancelable:true,
		composed:true,
		view,
		clientX:startX,
		clientY:startY,
	};
	
	// 模拟 mousedown
	const mouseDown = new MouseEvent('mousedown', eventOptions);
	el.dispatchEvent(mouseDown);
	
	// 模拟 mousemove
	const steps = 10;
	for (let i = 1; i<=steps; i++){
		const moveEvent = new MouseEvent('mousemove', {
			...eventOptions,
			clientX:startX+(deltaX*i)/steps,
			clientY:startY+(deltaY*i)/steps,
		});
		el.dispatchEvent(moveEvent);
	}
	
	// 模拟 mouseup
	const mouseUp = new MouseEvent('mouseup', {
		...eventOptions,
		clientX:startX+deltaX,
		clientY:startY+deltaY,
	});
	el.dispatchEvent(mouseUp);
}

// 在GM中会自动设置view为unsafeWindow，props指定自定义属性
function simulatePointerEvent(element, type, props = {}){
	if (!element || !(element instanceof HTMLElement)){
		throw new Error('需要一个有效的 DOM 元素');
	}
	const rect = element.getBoundingClientRect();
	
	// 默认中心点
	const centerX = rect.left+rect.width/2;
	const centerY = rect.top+rect.height/2;
	
	// 默认为 pointerdown 的配置，很多不支持的配置无法手动修改，构造函数也会忽略，
	const eventInit = {
		// =================== Event的属性 ================
		// eventPhase:2, // 刚到达目标元素
		bubbles:true,
		cancelable:true,
		composed:true,
		// target:element,
		// currentTarget:element,
		// srcElement:element, // 已经被弃用
		// ==================== UIEvent的属性 ================
		view:window,
		// ==================== MouseEvent的属性 ==============
		buttons:1,
		// x:centerX,
		// y:centerY,
		clientX:centerX,
		clientY:centerY,
		screenX:window.screenX+centerX,
		screenY:window.screenY+centerY,
		// pageX:centerX+window.scrollX,
		// pageY:centerY+window.scrollY,
		// offsetX:centerX-rect.left,
		// offsetY:centerY-rect.top,
		// layerX:Math.round(centerX),
		// layerY:Math.round(centerY),
		// ===================== PointerEvent的属性 ================
		pointerId:1,
		isPrimary:true,
		pointerType:'mouse',
		pressure:0.5,
		// persistentDeviceId:1,
	};
	
	if (type==='pointerdown'){
		delete eventInit.pressure;
		delete eventInit.buttons;
	} else if (type==='click'){
		delete eventInit.pressure;
		delete eventInit.isPrimary;
		delete eventInit.buttons;
		eventInit.details = 1;
	}
	
	// 检查GM环境
	if (typeof unsafeWindow!=='undefined'){
		eventInit.view = unsafeWindow;
	}
	
	const event = new PointerEvent(type, eventInit);
	Object.assign(element, props); // 合并自定义属性
	
	element.dispatchEvent(event);
}

// 比较原生事件和手动创建事件的不同，用于调试
function compareEvent(el){
	['pointerdown', 'pointerup', 'click'].forEach(type=>{
		el.addEventListener(type, event=>{
			const e = new PointerEvent(type);
			// 获取所有不同的字段
			const entries = getAllProperties(event).filter(name=>e[name]!==event[name])
																						 .map(name=>[name, event[name], e[name]]);
			console.log(type, entries);
		});
	});
	
	// 获取所有属性，包含原型链上的属性
	function getAllProperties(obj){
		const properties = new Set(); // 使用 Set 去重
		let currentObj = obj;
		
		// 遍历原型链
		do {
			// 获取当前对象的自身属性
			Object.getOwnPropertyNames(currentObj).forEach(prop=>properties.add(prop));
			// 移动到下一个原型
			currentObj = Object.getPrototypeOf(currentObj);
		} while (currentObj && currentObj!==Object.prototype); // 停止于 Object.prototype
		
		return Array.from(properties); // 转换为数组
	}
}

// 更新元素的内联属性，使用css文本或者对象，设置的属性值必须非空
// el: DOM元素
// styles: 可以是css字符串、对象
function setCss(el, styles){
	let styleObject = {};
	if (typeof styles==='object'){ // 解析对象为css字符串
		styleObject = styles;
	} else if (typeof styles==='string'){ // 解析 CSS 字符串为对象
		if (!styles.trim()){
			console.error('css字符串为空！');
			return;
		}
		const declarations = styles.trim()
															 .split(';').map(s=>s.trim()).filter(s=>s) // 分割声明
															 .map(x=>x.split(':').map(s=>s.trim())); // 分割属性和值
		for (const [k, v] of declarations){
			styleObject[k] = v;
		}
	} else {
		console.error('styles参数必须是对象或者字符串！');
		return;
	}
	
	for (const [k, v] of Object.entries(styleObject)){
		if (!k || !v){
			console.warn(`不合法的声明: ${k}: ${v}`);
			return;
		}
	}
	for (const [k, v] of Object.entries(styleObject)){
		if (v.includes('!important')){ // 如果包含!important，则使用重要性标记
			el.style.setProperty(k, v.replace('!important', '').trim(), 'important');
		} else {
			el.style.setProperty(k, v);
		}
	}
}

/** todo: 异步加载css和js，无需手动添加@require，但是就不能利用@require的缓存了
 *  todo: 考虑重构为highlight.js，不打包所有语言(体积太大)，而是动态加载语言插件
 * 高亮elements中每个元素，需要指定语言类型
 * - prism比hljs的标记更加精细，能识别函数名
 * @param {string|HTMLElement|HTMLElement[]|jQuery} selector 需要高亮的pre元素，会保证直接孩子是code元素
 * @param {string} language 语言类型
 */
export async function highlight(selector, language){
	// @require     https://cdn.jsdelivr.net/combine/npm/prismjs@1/components/prism-core.min.js,npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js,npm/prismjs@1/plugins/toolbar/prism-toolbar.min.js,npm/prismjs@1/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js
	// @resource    toolbarCss https://cdn.jsdelivr.net/npm/prismjs@1/plugins/toolbar/prism-toolbar.min.css
	addThemeSelect();
	
	$(selector)._each(e=>{
		const $e = $(e);
		$e.addClass('language-'+language);
		if ($e.children('code').length===0){
			$e.wrapInner(`<code>`);
		}
	});
	
	unsafeWindow.Prism = Prism; // autoloader运行在原始window环境中
	Prism.plugins.autoloader.languages_path = 'https://cdn.jsdelivr.net/npm/prismjs@1/components/';
	Prism.highlightAll();
	
	// 添加代码主题选择框
	async function addThemeSelect(){
		const themeCss = await gmStore.getOrSetAsync('prism-theme-css', fetchThemeCss);
		
		// 添加style元素
		const $style = $(`<style id="prism-theme-css-container">`).appendTo(document.head);
		
		// 构建options列表，并添加到页面
		const $select = $('<select id="prism-theme-select"></select>');
		for (const [name, css] of Object.entries(themeCss)){
			const $option = $(`<option>${name}</option>`);
			$option.data('css', css);
			$select.append($option);
		}
		$select.on('change', ()=>{
			const name = $select.val();
			$style.html(themeCss[name]);
			gmStore.set('lastTheme', name);
		}).appendTo(document.body);
		
		// 选择上一次的主题
		const lastTheme = gmStore.getOrSet('lastTheme', $select.val());
		$select.val(lastTheme).trigger('change');
		
		// 选择框和代码的样式
		GM_addStyle(/*language=css*/ `
      /* =========主题选择框========== */
      #prism-theme-select {
        all      : revert !important;
        position : fixed !important;
        right    : 0 !important;
        top      : 0 !important;
        z-index  : 999999 !important;

        opacity  : 0.1 !important;
      }

      #prism-theme-select:hover {
        opacity : 0.8 !important;
      }

      /* =========工具栏样式========== */
      ${GM_getResourceText('toolbarCss')}
		`);
		
		async function fetchThemeCss(){
			const origin = 'https://cdn.jsdelivr.net/';
			const themeSources = [
				'https://cdn.jsdelivr.net/npm/prismjs@1/themes/', /* 16个 */
				'https://cdn.jsdelivr.net/npm/prism-themes@1/themes/', /* 72个 */
			];
			
			// 获取基本主题的名称和链接
			const themeUrls = {};
			for (const source of themeSources){
				const $listDoc = await reqGM(source).jquery();
				$listDoc.find('.listing>table td.name:has(+td.size)>a').each((_, a)=>{
					const name = $(a).text();
					themeUrls[name] = new URL($(a).attr('href'), origin).toString();
				});
			}
			
			// 获取所有css文本
			const themeCss = {};
			await Promise.all(Object.entries(themeUrls).map( // Promise.all加快请求速度
					([name, url])=>reqGM(url).text().then(t=>themeCss[name] = t)), //
			);
			
			return themeCss;
		}
	}
}

// 加载fluent-ui库
export async function loadFluentUI(){
	const loadComponent = import('https://unpkg.com/@fluentui/web-components@beta');
	const loadTheme = (async()=>{
		const {setTheme} = await import('https://esm.run/@fluentui/web-components@beta');
		const {webLightTheme} = await import('https://esm.run/@fluentui/tokens');
		setTheme(webLightTheme);
	})();
	return Promise.all([loadComponent, loadTheme]);
}

// 加载shoelace-ui库
export async function loadShoelaceUI(){
	return Promise.all([
		import('https://esm.run/@shoelace-style/shoelace@2'),
		import('https://esm.run/@shoelace-style/shoelace@2/cdn/themes/light.css'),
	]);
}

// 从 https://esm.sh/ 加载esm模块
export async function importEsm(name){
	const url = `https://esm.run/${name}`;
	return import(url);
}

// 从esm.run加载css，会将css文件转换成esm进行加载，多次调用只会加载一次
export async function importCss(name){
	const url = `https://esm.run/${name}`;
	return import(url);
}
